<?php
define ('NUSOAP_LIBRARY_DIR', dirname(__FILE__) . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR, true);

// necessary classes
include_once(NUSOAP_LIBRARY_DIR . 'class.nusoap_base.php');
require_once(NUSOAP_LIBRARY_DIR . 'class.soapclient.php');
require_once(NUSOAP_LIBRARY_DIR . 'class.soap_val.php');
require_once(NUSOAP_LIBRARY_DIR . 'class.soap_parser.php');
require_once(NUSOAP_LIBRARY_DIR . 'class.soap_fault.php');

// transport classes
require_once(NUSOAP_LIBRARY_DIR . 'class.soap_transport_http.php');

// optional add-on classes
require_once(NUSOAP_LIBRARY_DIR . 'class.xmlschema.php');
require_once(NUSOAP_LIBRARY_DIR . 'class.wsdl.php');

// server class
require_once(NUSOAP_LIBRARY_DIR . 'class.soap_server.php');


class NSoapServerController extends CController {
	
	private $_server;
	private $_types;
	private $_className;
	private $_debugMode;
	private $typeMap=array(
			'string'=>'xsd:string',
			'str'=>'xsd:string',
			'int'=>'xsd:int',
			'integer'=>'xsd:integer',
			'float'=>'xsd:float',
			'double'=>'xsd:float',
			'bool'=>'xsd:boolean',
			'boolean'=>'xsd:boolean',
			'date'=>'xsd:date',
			'time'=>'xsd:time',
			'datetime'=>'xsd:dateTime',
			'array'=>'soap-enc:Array',
			'object'=>'xsd:struct',
			'mixed'=>'xsd:anyType',
		);

	/**
	 * @param string id of this controller
	 * @param CWebModule the module that this controller belongs to. This parameter
	 * has been available since version 1.0.3.
	 */
	public function __construct($id,$className,$module=null)
	{
		parent::__construct($id, $module);
		$this->_className = $className;
		$this->_debugMode = false;
	}

	public final function init() {
		$this->_server = new __private_soap_server($this);
		$this->_server->setDebugLevel(1 /* $this->_debugMode ? 1 : 0 */);
		$this->_server->configureWSDL($this->getServiceName(), $this->getWsdlUrl());
	    $this->_server->soap_defencoding = 'UTF-8';
	    $this->_server->wsdl->soap_defencoding = 'UTF-8';
		$this->_server->decode_utf8 = false;
		$this->_types = array();
		
		$reflection=new ReflectionClass($this->getClassName());
		foreach($reflection->getMethods() as $method)
		{
			if($method->isPublic())
				$this->registerMethod($method);
		}
		
		foreach ($this->_types as $name => $type) {
			if (is_array($type)) {
				$this->_server->wsdl->addComplexType(
				    $name,
				    'complexType',
				    'struct',
				    'all',
				    '',
				    $type
				);
			} else {
				$srctype = ereg_replace('^[a-z]+:', '', ereg_replace('Array$', '', $type));
				$name = ereg_replace('^[a-z]+:', '', $type);
				if (array_key_exists($srctype, $this->typeMap)) {
					$srctype = $this->typeMap[$srctype];
				} else {
					$srctype = "tns:" . $srctype;
				}
				
				$this->_server->wsdl->addComplexType(
				    $name,
				    'complexType',
				    'array',
				    '',
				    'SOAP-ENC:Array',
				    array(),
					array(array('ref'=>'SOAP-ENC:arrayType', 'wsdl:arrayType'=>$srctype.'[]')),
					$srctype
				);
			}
		}
	}
	
	public final function action(){
		return array('index');
	}
	
	public final function actionIndex() {
		$HTTP_RAW_POST_DATA = file_get_contents("php://input"); 
		if ($this->_debugMode) {
			Yii::log($HTTP_RAW_POST_DATA, CLogger::LEVEL_ERROR, "WebService");
		}
		$this->_server->service($HTTP_RAW_POST_DATA);
		if ($this->_debugMode) {
			Yii::log($this->_server->getDebug(), CLogger::LEVEL_ERROR, "WebService");
		}
	}
	
	public final function getServiceName() {
		return ucwords($this->id)."Service";
	}
	
	public final function getWsdlUrl() {
		return 'urn:'.$this->getServiceName();
	}
	
	public function getClassName() {
		return $this->_className;
	}
	
	public function serDebugMode($debugMode) {
		$this->_debugMode = ($debugMode === true);
	}

	private function registerMethod($method)
	{
		$comment=$method->getDocComment();
		if(strpos($comment,'@soap')===false)
			return;

		$methodName=$method->getName();
		$comment=preg_replace('/^\s*\**(\s*?$|\s*)/m','',$comment);
		$params=$method->getParameters();
		$in=array();
		$n=preg_match_all('/^@param\s+([\w\.]+(\[\s*\])?)\s*?(.*)$/im',$comment,$matches);
		if($n>count($params))
			$n=count($params);
		for($i=0;$i<$n;++$i)
			$in[$params[$i]->getName()]=$this->registerType($matches[1][$i]); // name => type 

		$return=array();
		if(preg_match('/^@return\s+([\w\.]+(\[\s*\])?)\s*?(.*)$/im',$comment,$matches))
			$return['return'] = $this->registerType($matches[1]); // type
		
		if(preg_match('/^\/\*+\s*([^@]*?)\n@/s',$comment,$matches))
			$doc=trim($matches[1]);
		else
			$doc='';
			
		// Register the method to expose
		$this->_server->register($this->getServiceName().'.'.$methodName, // method name
		    $in,                                                          // input parameters
		    $return,                                                      // output parameters
		    $this->getWsdlUrl(),                                          // namespace
		    $this->getWsdlUrl().'#'.$methodName,                          // soapaction
		    'rpc',                                                        // style
		    'encoded',                                                    // use
		    $doc                                                          // documentation
		);
	}
	
	private function registerType($type)
	{
		if(isset($this->typeMap[$type]))
			return $this->typeMap[$type];
		else if(isset($this->_types[$type]))
			return is_array($this->_types[$type]) ? 'tns:'.$type : $this->_types[$type];
		else if(($pos=strpos($type,'[]'))!==false) // if it is an array
		{
			$type=substr($type,0,$pos);
			if(isset($this->typeMap[$type]))
				$this->_types[$type.'[]']='tns:'.$type.'Array';
			else
			{
				$this->_types[$type.'[]']='tns:'.$type.'Array';
				$this->registerType($type);
			}
			return $this->_types[$type.'[]'];
		}
		else // class type
		{
			$type=Yii::import($type,true);
			$this->_types[$type]=array();
			$class=new ReflectionClass($type);
			foreach($class->getProperties() as $property)
			{
				$comment=$property->getDocComment();
				if($property->isPublic() && strpos($comment,'@soap')!==false)
				{
					if(preg_match('/@var\s+([\w\.]+(\[\s*\])?)\s*?(.*)$/mi',$comment,$matches))
						$this->_types[$type][$property->getName()]=array('name' => $property->getName(), 'type' => $this->registerType($matches[1]));  // name => type, doc
				}
			}
			return 'tns:'.$type;
		}
	}
	
	
	
}

class __private_soap_server extends nusoap_server {
	
	private $service;
	
	public function __construct($service) {
		parent::nusoap_server();
		$this->service = $service;
		
		if ($service == null) {
			throw new CException("Web Service can't be null", 500);
		}
		
		if (! ($service instanceof NSoapServerController)) {
			throw new CException("Web Service must be NSoapServerController (actually ".get_class($service).")", 500);
		}
	}
	
	/**
	 * invokes a PHP function for the requested SOAP method
	 *
	 * The following fields are set by this function (when successful)
	 *
	 * methodreturn
	 *
	 * Note that the PHP function that is called may also set the following
	 * fields to affect the response sent to the client
	 *
	 * responseHeaders
	 * outgoing_headers
	 *
	 * This sets the fault field on error
	 *
	 * @access   private
	 */
	function invoke_method() {
		try {
			$this->debug('in invoke_method, methodname=' . $this->methodname . ' methodURI=' . $this->methodURI . ' SOAPAction=' . $this->SOAPAction);
	
			//
			// if you are debugging in this area of the code, your service uses a class to implement methods,
			// you use SOAP RPC, and the client is .NET, please be aware of the following...
			// when the .NET wsdl.exe utility generates a proxy, it will remove the '.' or '..' from the
			// method name.  that is fine for naming the .NET methods.  it is not fine for properly constructing
			// the XML request and reading the XML response.  you need to add the RequestElementName and
			// ResponseElementName to the System.Web.Services.Protocols.SoapRpcMethodAttribute that wsdl.exe
			// generates for the method.  these parameters are used to specify the correct XML element names
			// for .NET to use, i.e. the names with the '.' in them.
			//
			$orig_methodname = $this->methodname;
			if ($this->wsdl) {
				if ($this->opData = $this->wsdl->getOperationData($this->methodname)) {
					$this->debug('in invoke_method, found WSDL operation=' . $this->methodname);
					$this->appendDebug('opData=' . $this->varDump($this->opData));
				} elseif ($this->opData = $this->wsdl->getOperationDataForSoapAction($this->SOAPAction)) {
					// Note: hopefully this case will only be used for doc/lit, since rpc services should have wrapper element
					$this->debug('in invoke_method, found WSDL soapAction=' . $this->SOAPAction . ' for operation=' . $this->opData['name']);
					$this->appendDebug('opData=' . $this->varDump($this->opData));
					$this->methodname = $this->opData['name'];
				} else {
					$this->debug('in invoke_method, no WSDL for operation=' . $this->methodname);
					$this->fault('SOAP-ENV:Client', "Operation '" . $this->methodname . "' is not defined in the WSDL for this service");
					return;
				}
			} else {
				$this->debug('in invoke_method, no WSDL to validate method');
			}
	
			// if a . is present in $this->methodname, we see if there is a class in scope,
			// which could be referred to. We will also distinguish between two deliminators,
			// to allow methods to be called a the class or an instance
			if (strpos($this->methodname, '..') > 0) {
				$delim = '..';
			} else if (strpos($this->methodname, '.') > 0) {
				$delim = '.';
			} else {
				$delim = '';
			}
			$this->debug("in invoke_method, delim=$delim");
	
			$class = '';
			$method = '';
			if (strlen($delim) > 0 && substr_count($this->methodname, $delim) == 1) {
				$try_class = substr($this->methodname, 0, strpos($this->methodname, $delim));
				$this->debug("in invoke_method, try_class=$try_class serviceName=".$this->service->serviceName);
				if ($try_class == $this->service->serviceName) {
					$class = $this->service->className;
					$method = substr($this->methodname, strpos($this->methodname, $delim) + strlen($delim));
					$this->debug("in invoke_method, class=$class method=$method delim=$delim");
				} elseif (class_exists($try_class)) {
					// get the class and method name
					$class = $try_class;
					$method = substr($this->methodname, strpos($this->methodname, $delim) + strlen($delim));
					$this->debug("in invoke_method, class=$class method=$method delim=$delim");
				} else {
					$this->debug("in invoke_method, class=$try_class not found");
				}
			} else {
				$try_class = '';
				$this->debug("in invoke_method, no class to try");
			}
	
			// does method exist?
			if ($class == '') {
				if (!function_exists($this->methodname)) {
					$this->debug("in invoke_method, function '$this->methodname' not found!");
					$this->result = 'fault: method not found';
					$this->fault('SOAP-ENV:Client',"method '$this->methodname'('$orig_methodname') not defined in service('$try_class' '$delim')");
					return;
				}
			} else {
				$method_to_compare = (substr(phpversion(), 0, 2) == '4.') ? strtolower($method) : $method;
				if (!in_array($method_to_compare, get_class_methods($class))) {
					$this->debug("in invoke_method, method '$this->methodname' not found in class '$class'!");
					$this->result = 'fault: method not found';
					$this->fault('SOAP-ENV:Client',"method '$this->methodname'/'$method_to_compare'('$orig_methodname') not defined in service/'$class'('$try_class' '$delim')");
					return;
				}
			}
	
			// evaluate message, getting back parameters
			// verify that request parameters match the method's signature
			if(! $this->verify_method($this->methodname,$this->methodparams)){
				// debug
				$this->debug('ERROR: request not verified against method signature');
				$this->result = 'fault: request failed validation against method signature';
				// return fault
				$this->fault('SOAP-ENV:Client',"Operation '$this->methodname' not defined in service.");
				return;
			}
	
			// if there are parameters to pass
			$this->debug('in invoke_method, params:');
			$this->appendDebug($this->varDump($this->methodparams));
			$this->debug("in invoke_method, calling '$this->methodname'");
			if (!function_exists('call_user_func_array')) {
				if ($class == '') {
					$this->debug('in invoke_method, calling function using eval()');
					$funcCall = "\$this->methodreturn = $this->methodname(";
				} else {
					if ($delim == '..') {
						$this->debug('in invoke_method, calling class method using eval()');
						$funcCall = "\$this->methodreturn = ".$class."::".$method."(";
					} elseif ($class == $this->service->className) {
						$this->debug('in invoke_method, calling service method using eval()');
						$funcCall = "\$this->methodreturn = \$this->service->".$method."(";
					} else {
						$this->debug('in invoke_method, calling instance method using eval()');
						// generate unique instance name
						$instname = "\$inst_".time();
						$funcCall = $instname." = new ".$class."(); ";
						$funcCall .= "\$this->methodreturn = ".$instname."->".$method."(";
					}
				}
				if ($this->methodparams) {
					foreach ($this->methodparams as $param) {
						if (is_array($param) || is_object($param)) {
							$this->fault('SOAP-ENV:Client', 'NuSOAP does not handle complexType parameters correctly when using eval; call_user_func_array must be available');
							return;
						}
						$funcCall .= "\"$param\",";
					}
					$funcCall = substr($funcCall, 0, -1);
				}
				$funcCall .= ');';
				$this->debug('in invoke_method, function call: '.$funcCall);
				@eval($funcCall);
			} else {
				if ($class == '') {
					$this->debug('in invoke_method, calling function using call_user_func_array()');
					$call_arg = "$this->methodname";	// straight assignment changes $this->methodname to lower case after call_user_func_array()
				} elseif ($delim == '..') {
					$this->debug('in invoke_method, calling class method using call_user_func_array()');
					$call_arg = array ($class, $method);
				} elseif ($class == $this->service->className) {
					$this->debug('in invoke_method, calling service method using call_user_func_array()');
					$instance = $this->service;
					$call_arg = array(&$instance, $method);
				} else {
					$this->debug('in invoke_method, calling instance method using call_user_func_array()');
					$instance = new $class ();
					$call_arg = array(&$instance, $method);
				}
				if (is_array($this->methodparams)) {
					$this->methodreturn = call_user_func_array($call_arg, array_values($this->methodparams));
				} else {
					$this->methodreturn = call_user_func_array($call_arg, array());
				}
			}
	        $this->debug('in invoke_method, methodreturn:');
	        $this->appendDebug($this->varDump($this->methodreturn));
			$this->debug("in invoke_method, called method $this->methodname, received data of type ".gettype($this->methodreturn));
		/*} catch(CException $cex) {
			$this->fault($cex->code, $cex->message, '', $cex->getTrace());*/
		} catch(Exception $ex) {
			$this->fault($ex->getCode(), $ex->getMessage());
		}
	}
	
}

